/*
 * Copyright 2002-2013 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.security.config.annotation;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.util.Assert;
import org.springframework.web.filter.DelegatingFilterProxy;

/**
 * <p>A base {@link SecurityBuilder} that allows {@link SecurityConfigurer} to be
 * applied to it. This makes modifying the {@link SecurityBuilder} a strategy
 * that can be customized and broken up into a number of
 * {@link SecurityConfigurer} objects that have more specific goals than that
 * of the {@link SecurityBuilder}.</p>
 *
 * <p>For example, a {@link SecurityBuilder} may build an
 * {@link DelegatingFilterProxy}, but a {@link SecurityConfigurer} might
 * populate the {@link SecurityBuilder} with the filters necessary for session
 * management, form based login, authorization, etc.</p>
 *
 * @see WebSecurity
 *
 * @author Rob Winch
 *
 * @param <O>
 *            The object that this builder returns
 * @param <B>
 *            The type of this builder (that is returned by the base class)
 */
public abstract class AbstractConfiguredSecurityBuilder<O, B extends SecurityBuilder<O>> extends AbstractSecurityBuilder<O> {
    private final Log logger = LogFactory.getLog(getClass());

    private final LinkedHashMap<Class<? extends SecurityConfigurer<O, B>>, List<SecurityConfigurer<O, B>>> configurers =
            new LinkedHashMap<Class<? extends SecurityConfigurer<O, B>>, List<SecurityConfigurer<O, B>>>();

    private final Map<Class<Object>,Object> sharedObjects = new HashMap<Class<Object>,Object>();

    private final boolean allowConfigurersOfSameType;

    private BuildState buildState = BuildState.UNBUILT;

    private ObjectPostProcessor<Object> objectPostProcessor;

    /***
     * Creates a new instance with the provided {@link ObjectPostProcessor}.
     * This post processor must support Object since there are many types of
     * objects that may be post processed.
     *
     * @param objectPostProcessor the {@link ObjectPostProcessor} to use
     */
    protected AbstractConfiguredSecurityBuilder(ObjectPostProcessor<Object> objectPostProcessor) {
        this(objectPostProcessor,false);
    }

    /***
     * Creates a new instance with the provided {@link ObjectPostProcessor}.
     * This post processor must support Object since there are many types of
     * objects that may be post processed.
     *
     * @param objectPostProcessor the {@link ObjectPostProcessor} to use
     * @param allowConfigurersOfSameType if true, will not override other {@link SecurityConfigurer}'s when performing apply
     */
    protected AbstractConfiguredSecurityBuilder(ObjectPostProcessor<Object> objectPostProcessor, boolean allowConfigurersOfSameType) {
        Assert.notNull(objectPostProcessor, "objectPostProcessor cannot be null");
        this.objectPostProcessor = objectPostProcessor;
        this.allowConfigurersOfSameType = allowConfigurersOfSameType;
    }


    /**
     * Similar to {@link #build()} and {@link #getObject()} but checks the state
     * to determine if {@link #build()} needs to be called first.
     *
     * @return the result of {@link #build()} or {@link #getObject()}. If an
     *         error occurs while building, returns null.
     */
    public O getOrBuild() {
        if(isUnbuilt()) {
            try {
                return build();
            } catch(Exception e) {
                logger.debug("Failed to perform build. Returning null", e);
                return null;
            }
        } else {
            return getObject();
        }
    }

    /**
     * Applies a {@link SecurityConfigurerAdapter} to this
     * {@link SecurityBuilder} and invokes
     * {@link SecurityConfigurerAdapter#setBuilder(SecurityBuilder)}.
     *
     * @param configurer
     * @return
     * @throws Exception
     */
    @SuppressWarnings("unchecked")
    public <C extends SecurityConfigurerAdapter<O, B>> C apply(C configurer)
            throws Exception {
        add(configurer);
        configurer.addObjectPostProcessor(objectPostProcessor);
        configurer.setBuilder((B) this);
        return configurer;
    }

    /**
     * Applies a {@link SecurityConfigurer} to this {@link SecurityBuilder}
     * overriding any {@link SecurityConfigurer} of the exact same class. Note
     * that object hierarchies are not considered.
     *
     * @param configurer
     * @return
     * @throws Exception
     */
    public <C extends SecurityConfigurer<O, B>> C apply(C configurer)
            throws Exception {
        add(configurer);
        return configurer;
    }

    /**
     * Sets an object that is shared by multiple {@link SecurityConfigurer}.
     *
     * @param sharedType the Class to key the shared object by.
     * @param object the Object to store
     */
    @SuppressWarnings("unchecked")
    public <C> void setSharedObject(Class<C> sharedType, C object) {
        this.sharedObjects.put((Class<Object>) sharedType, object);
    }

    /**
     * Gets a shared Object. Note that object heirarchies are not considered.
     *
     * @param sharedType the type of the shared Object
     * @return the shared Object or null if it is not found
     */
    @SuppressWarnings("unchecked")
    public <C> C getSharedObject(Class<C> sharedType) {
        return (C) this.sharedObjects.get(sharedType);
    }

    /**
     * Gets the shared objects
     * @return
     */
    public Map<Class<Object>,Object> getSharedObjects() {
        return Collections.unmodifiableMap(this.sharedObjects);
    }

    /**
     * Adds {@link SecurityConfigurer} ensuring that it is allowed and
     * invoking {@link SecurityConfigurer#init(SecurityBuilder)} immediately
     * if necessary.
     *
     * @param configurer the {@link SecurityConfigurer} to add
     * @throws Exception if an error occurs
     */
    @SuppressWarnings("unchecked")
    private <C extends SecurityConfigurer<O, B>> void add(C configurer) throws Exception {
        Assert.notNull(configurer, "configurer cannot be null");

        Class<? extends SecurityConfigurer<O, B>> clazz = (Class<? extends SecurityConfigurer<O, B>>) configurer
                .getClass();
        synchronized(configurers) {
            if(buildState.isConfigured()) {
                throw new IllegalStateException("Cannot apply "+configurer+" to already built object");
            }
            List<SecurityConfigurer<O, B>> configs = allowConfigurersOfSameType ? this.configurers.get(clazz) : null;
            if(configs == null) {
                configs = new ArrayList<SecurityConfigurer<O,B>>(1);
            }
            configs.add(configurer);
            this.configurers.put(clazz, configs);
            if(buildState.isInitializing()) {
                configurer.init((B)this);
            }
        }
    }

    /**
     * Gets all the {@link SecurityConfigurer} instances by its class name or an
     * empty List if not found. Note that object hierarchies are not considered.
     *
     * @param clazz the {@link SecurityConfigurer} class to look for
     * @return
     */
    @SuppressWarnings("unchecked")
    public <C extends SecurityConfigurer<O, B>> List<C> getConfigurers(
            Class<C> clazz) {
        List<C> configs = (List<C>) this.configurers.get(clazz);
        if(configs == null) {
            return new ArrayList<C>();
        }
        return new ArrayList<C>(configs);
    }

    /**
     * Removes all the {@link SecurityConfigurer} instances by its class name or an
     * empty List if not found. Note that object hierarchies are not considered.
     *
     * @param clazz the {@link SecurityConfigurer} class to look for
     * @return
     */
    @SuppressWarnings("unchecked")
    public <C extends SecurityConfigurer<O, B>> List<C> removeConfigurers(
            Class<C> clazz) {
        List<C> configs = (List<C>) this.configurers.remove(clazz);
        if(configs == null) {
            return new ArrayList<C>();
        }
        return new ArrayList<C>(configs);
    }

    /**
     * Gets the {@link SecurityConfigurer} by its class name or
     * <code>null</code> if not found. Note that object hierarchies are not
     * considered.
     *
     * @param clazz
     * @return
     */
    @SuppressWarnings("unchecked")
    public <C extends SecurityConfigurer<O, B>> C getConfigurer(
            Class<C> clazz) {
        List<SecurityConfigurer<O,B>> configs = this.configurers.get(clazz);
        if(configs == null) {
            return null;
        }
        if(configs.size() != 1) {
            throw new IllegalStateException("Only one configurer expected for type " + clazz + ", but got " + configs);
        }
        return (C) configs.get(0);
    }

    /**
     * Removes and returns the {@link SecurityConfigurer} by its class name or
     * <code>null</code> if not found. Note that object hierarchies are not
     * considered.
     *
     * @param clazz
     * @return
     */
    @SuppressWarnings("unchecked")
    public <C extends SecurityConfigurer<O,B>> C removeConfigurer(Class<C> clazz) {
        List<SecurityConfigurer<O,B>> configs = this.configurers.remove(clazz);
        if(configs == null) {
            return null;
        }
        if(configs.size() != 1) {
            throw new IllegalStateException("Only one configurer expected for type " + clazz + ", but got " + configs);
        }
        return (C) configs.get(0);
    }

    /**
     * Specifies the {@link ObjectPostProcessor} to use.
     * @param objectPostProcessor the {@link ObjectPostProcessor} to use. Cannot be null
     * @return the {@link SecurityBuilder} for further customizations
     */
    @SuppressWarnings("unchecked")
    public O objectPostProcessor(ObjectPostProcessor<Object> objectPostProcessor) {
        Assert.notNull(objectPostProcessor,"objectPostProcessor cannot be null");
        this.objectPostProcessor = objectPostProcessor;
        return (O) this;
    }

    /**
     * Performs post processing of an object. The default is to delegate to the
     * {@link ObjectPostProcessor}.
     *
     * @param object the Object to post process
     * @return the possibly modified Object to use
     */
    protected <P> P postProcess(P object) {
        return (P) this.objectPostProcessor.postProcess(object);
    }

    /**
     * Executes the build using the {@link SecurityConfigurer}'s that have been applied using the following steps:
     *
     * <ul>
     * <li>Invokes {@link #beforeInit()} for any subclass to hook into</li>
     * <li>Invokes {@link SecurityConfigurer#init(SecurityBuilder)} for any {@link SecurityConfigurer} that was applied to this builder.</li>
     * <li>Invokes {@link #beforeConfigure()} for any subclass to hook into</li>
     * <li>Invokes {@link #performBuild()} which actually builds the Object</li>
     * </ul>
     */
    @Override
    protected final O doBuild() throws Exception {
        synchronized(configurers) {
            buildState = BuildState.INITIALIZING;

            beforeInit();
            init();

            buildState = BuildState.CONFIGURING;

            beforeConfigure();
            configure();

            buildState = BuildState.BUILDING;

            O result = performBuild();

            buildState = BuildState.BUILT;

            return result;
        }
    }

    /**
     * Invoked prior to invoking each
     * {@link SecurityConfigurer#init(SecurityBuilder)} method. Subclasses may
     * override this method to hook into the lifecycle without using a
     * {@link SecurityConfigurer}.
     */
    protected void beforeInit() throws Exception {
    }

    /**
     * Invoked prior to invoking each
     * {@link SecurityConfigurer#configure(SecurityBuilder)} method.
     * Subclasses may override this method to hook into the lifecycle without
     * using a {@link SecurityConfigurer}.
     */
    protected void beforeConfigure() throws Exception {
    }

    /**
     * Subclasses must implement this method to build the object that is being returned.
     *
     * @return
     */
    protected abstract O performBuild() throws Exception;

    @SuppressWarnings("unchecked")
    private void init() throws Exception {
        Collection<SecurityConfigurer<O,B>> configurers = getConfigurers();

        for(SecurityConfigurer<O,B> configurer : configurers ) {
            configurer.init((B) this);
        }
    }

    @SuppressWarnings("unchecked")
    private void configure() throws Exception {
        Collection<SecurityConfigurer<O,B>> configurers = getConfigurers();

        for(SecurityConfigurer<O,B> configurer : configurers ) {
            configurer.configure((B) this);
        }
    }

    private Collection<SecurityConfigurer<O, B>> getConfigurers() {
        List<SecurityConfigurer<O,B>> result = new ArrayList<SecurityConfigurer<O,B>>();
        for(List<SecurityConfigurer<O,B>> configs : this.configurers.values()) {
            result.addAll(configs);
        }
        return result;
    }

    /**
     * Determines if the object is unbuilt.
     * @return true, if unbuilt else false
     */
    private boolean isUnbuilt() {
        synchronized(configurers) {
            return buildState == BuildState.UNBUILT;
        }
    }

    /**
     * The build state for the application
     *
     * @author Rob Winch
     * @since 3.2
     */
    private static enum BuildState {
        /**
         * This is the state before the {@link Builder#build()} is invoked
         */
        UNBUILT(0),

        /**
         * The state from when {@link Builder#build()} is first invoked until
         * all the {@link SecurityConfigurer#init(SecurityBuilder)} methods
         * have been invoked.
         */
        INITIALIZING(1),

        /**
         * The state from after all
         * {@link SecurityConfigurer#init(SecurityBuilder)} have been invoked
         * until after all the
         * {@link SecurityConfigurer#configure(SecurityBuilder)} methods have
         * been invoked.
         */
        CONFIGURING(2),

        /**
         * From the point after all the
         * {@link SecurityConfigurer#configure(SecurityBuilder)} have
         * completed to just after
         * {@link AbstractConfiguredSecurityBuilder#performBuild()}.
         */
        BUILDING(3),

        /**
         * After the object has been completely built.
         */
        BUILT(4);

        private final int order;

        BuildState(int order) {
            this.order = order;
        }

        public boolean isInitializing() {
            return INITIALIZING.order == order;
        }

        /**
         * Determines if the state is CONFIGURING or later
         * @return
         */
        public boolean isConfigured() {
            return order >= CONFIGURING.order;
        }
    }
}